{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8c5bd1ca-3329-4062-8851-9bd33009d805",
   "metadata": {},
   "source": [
    "# Using the sharing API from Python \n",
    "\n",
    "In this example, we use $JUPYTERHUB_API_TOKEN to communicate with the sharing API via Python.\n",
    "\n",
    "The permissions used here are granted via the `c.Spawner.server_token_scopes` config in jupyterhub_config.py\n",
    "\n",
    "By using this token, any user who has access to this server has access to sharing permissions.\n",
    "\n",
    "First, get some useful configuration from the server environment:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "95fa629b-ac65-46da-86a6-9798f3d0a2ba",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'http://127.0.0.1:8081/hub/api'"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import os\n",
    "\n",
    "hub_api = os.environ[\"JUPYTERHUB_API_URL\"]\n",
    "token = os.environ[\"JUPYTERHUB_API_TOKEN\"]\n",
    "username = os.environ[\"JUPYTERHUB_USER\"]\n",
    "user_server = f\"{username}/{os.environ['JUPYTERHUB_SERVER_NAME']}\"\n",
    "hub_host = os.environ[\"JUPYTERHUB_HOST\"]\n",
    "server_base_url = os.environ[\"JUPYTERHUB_SERVICE_PREFIX\"]\n",
    "\n",
    "hub_api"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "456697ab-e7ce-4f75-bc8f-3ce424830e0d",
   "metadata": {},
   "source": [
    "Create a requests.Session to make jupyterhub API requests with our token"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f3335eeb-64e5-4caa-acb5-1032aaf727bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "session = requests.Session()\n",
    "session.headers = {\"Authorization\": f\"Bearer {token}\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cf5605b-2022-4d2a-b0cc-de6f80e8f2fe",
   "metadata": {},
   "source": [
    "We can check the permissions our token has with a request to /hub/api/user:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "17529af3-5ec8-4495-9183-bd5d423a1d76",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'kind': 'user',\n",
       " 'last_activity': '2024-01-23T11:43:50.800864Z',\n",
       " 'groups': [],\n",
       " 'admin': False,\n",
       " 'name': 'sharer',\n",
       " 'servers': {'': {'name': '',\n",
       "   'full_name': 'sharer/',\n",
       "   'last_activity': '2024-01-23T11:43:50.800864Z',\n",
       "   'started': '2024-01-23T11:28:44.948553Z',\n",
       "   'pending': None,\n",
       "   'ready': True,\n",
       "   'stopped': False,\n",
       "   'url': '/user/sharer/',\n",
       "   'user_options': {},\n",
       "   'progress_url': '/hub/api/users/sharer/server/progress'}},\n",
       " 'session_id': None,\n",
       " 'scopes': ['access:servers!server=sharer/',\n",
       "  'delete:servers!server=sharer/',\n",
       "  'groups:shares!server=sharer/',\n",
       "  'read:groups:shares!server=sharer/',\n",
       "  'read:servers!server=sharer/',\n",
       "  'read:shares!server=sharer/',\n",
       "  'read:users:activity!user=sharer',\n",
       "  'read:users:groups!user=sharer',\n",
       "  'read:users:name!user=sharer',\n",
       "  'servers!server=sharer/',\n",
       "  'shares!server=sharer/',\n",
       "  'users:activity!server=sharer/',\n",
       "  'users:activity!user=sharer',\n",
       "  'users:shares!server=sharer/']}"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "r = session.get(f\"{hub_api}/user\")\n",
    "r.json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a14578b8-577e-4b0a-b74c-40d7a04a1a1a",
   "metadata": {},
   "source": [
    "We can see who has access to this server:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "63520e3c-3621-4ebf-9775-52e6150ccca5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "shares_url = f\"{hub_api}/shares/{user_server}\"\n",
    "share_codes_url = f\"{hub_api}/share-codes/{user_server}\"\n",
    "r = session.get(shares_url)\n",
    "r.json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c8870d0-5b7e-4065-b227-fd463289cfdf",
   "metadata": {},
   "source": [
    "and if there are any outstanding codes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6fad5334-6034-48f8-b008-1fd3d6e4d139",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "r = session.get(share_codes_url)\n",
    "r.json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6d598b3-5eab-4d56-b2ea-9aec345a3948",
   "metadata": {},
   "source": [
    "Next, we can create a code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "facb1872-44a5-4e4e-84d4-1fd829b1a27b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'server': {'user': {'name': 'sharer'},\n",
       "  'name': '',\n",
       "  'url': '/user/sharer/',\n",
       "  'ready': True},\n",
       " 'scopes': ['access:servers!server=sharer/'],\n",
       " 'id': 'sc_1',\n",
       " 'created_at': '2024-01-23T11:46:32.154416Z',\n",
       " 'expires_at': '2024-01-24T11:46:32.153582Z',\n",
       " 'exchange_count': 0,\n",
       " 'last_exchanged_at': None,\n",
       " 'code': 'gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg',\n",
       " 'accept_url': '/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg'}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "r = session.post(share_codes_url)\n",
    "code_info = r.json()\n",
    "code_info"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "83e23f83-18cb-4e54-9683-68c2ab0fcfc2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "    <a href='/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg'>share this link to grant access</a>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import HTML, display\n",
    "\n",
    "full_accept_url = f\"{hub_host}{code_info['accept_url']}\"\n",
    "\n",
    "display(\n",
    "    HTML(f\"\"\"\n",
    "    <a href='{full_accept_url}'>share this link to grant access</a>\n",
    "    \"\"\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ce4f6f8-a0f3-4556-ab25-1f86ef49a6db",
   "metadata": {},
   "source": [
    "(in jupyterlab, shift-right-click to copy link)\n",
    "\n",
    "We can now give this to the shared-with user (i.e. us in another private browsing tab).\n",
    "\n",
    "After accepting the link, we can see who we've shared with again:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "e056a9f0-7cec-414b-a3ce-03c8abfbc087",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [{'server': {'user': {'name': 'sharer'},\n",
       "    'name': '',\n",
       "    'url': '/user/sharer/',\n",
       "    'ready': True},\n",
       "   'scopes': ['access:servers!server=sharer/'],\n",
       "   'user': {'name': 'shared-with'},\n",
       "   'group': None,\n",
       "   'kind': 'user',\n",
       "   'created_at': '2024-01-23T11:46:43.585455Z'}],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.get(shares_url).json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9933ef79-3f59-45fc-92a9-13e7bc1f3b82",
   "metadata": {},
   "source": [
    "The share code can also include a `?next=` url parameter, to enable a link to take users to a specific file or view after accepting the code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "9c313d99-4dc9-45af-9643-f2b0e08b2bb4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg&next=%2Fuser%2Fsharer%2Flab%2Ftree%2Fshare-api.ipynb\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    share <a href='/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg&next=%2Fuser%2Fsharer%2Flab%2Ftree%2Fshare-api.ipynb'>this link</a>\n",
       "    to grant access and direct users to this notebook\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from urllib.parse import urlencode\n",
    "\n",
    "this_notebook_url = server_base_url + \"lab/tree/share-api.ipynb\"\n",
    "this_notebook_accept_url = (\n",
    "    full_accept_url + \"&\" + urlencode({\"next\": this_notebook_url})\n",
    ")\n",
    "print(this_notebook_accept_url)\n",
    "\n",
    "display(\n",
    "    HTML(f\"\"\"\n",
    "    share <a href='{this_notebook_accept_url}'>this link</a>\n",
    "    to grant access and direct users to this notebook\n",
    "    \"\"\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2455ba21-f558-4391-b8fc-6501feb3bbfb",
   "metadata": {},
   "source": [
    "## Reviewing and managing access\n",
    "\n",
    "Listing share codes doesn't reveal the code - if you need to get a code, issue a new sharing code.\n",
    "\n",
    "But we can see in `exchange_count` whether and how often the code has been used"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "313116ed-2aa5-4a0a-bc91-f15a9428ae0c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [{'server': {'user': {'name': 'sharer'},\n",
       "    'name': '',\n",
       "    'url': '/user/sharer/',\n",
       "    'ready': True},\n",
       "   'scopes': ['access:servers!server=sharer/'],\n",
       "   'id': 'sc_1',\n",
       "   'created_at': '2024-01-23T11:46:32.154416Z',\n",
       "   'expires_at': '2024-01-24T11:46:32.153582Z',\n",
       "   'exchange_count': 1,\n",
       "   'last_exchanged_at': '2024-01-23T11:46:43.589701Z'}],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.get(share_codes_url).json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f0d4d8e-fabb-4423-b9bd-d96643a9b9f7",
   "metadata": {},
   "source": [
    "we can also revoke the code. Codes can be deleted by code or id"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "666ac28c-70f5-4a75-9bfa-6d26c5ea610f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [204]>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.delete(share_codes_url + f\"?id={code_info['id']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce2fdb7f-a2de-4c95-996b-527ce0536794",
   "metadata": {},
   "source": [
    "or if you're done sharing via code, you can delete all sharing codes for a server without looking it up their ids:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "4633b0af-ad75-4e72-9702-9cb16182aecb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [204]>"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.delete(share_codes_url)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c322e058-bbf5-4058-8e20-415c7da0aa8a",
   "metadata": {},
   "source": [
    "scopes and expiration can be customized in the request when creating the share code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "462bcec7-a899-4e3e-8e77-7d08adedb7a4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [200]>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import json\n",
    "\n",
    "options = {\n",
    "    \"scopes\": [\n",
    "        f\"access:servers!server={user_server}\",  # access the server (default)\n",
    "        f\"servers!server={user_server}\",  # start/stop the server\n",
    "        f\"shares!server={user_server}\",  # further share the server with others\n",
    "    ],\n",
    "    \"expires_in\": 3600,  # code expires in one hour\n",
    "}\n",
    "\n",
    "\n",
    "session.post(share_codes_url, data=json.dumps(options))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "c8babe6c-6339-4990-9241-d3db54205108",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [{'server': {'user': {'name': 'sharer'},\n",
       "    'name': '',\n",
       "    'url': '/user/sharer/',\n",
       "    'ready': True},\n",
       "   'scopes': ['access:servers!server=sharer/'],\n",
       "   'user': {'name': 'shared-with'},\n",
       "   'group': None,\n",
       "   'kind': 'user',\n",
       "   'created_at': '2024-01-23T11:46:43.585455Z'}],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "r = session.get(shares_url)\n",
    "r.json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "616d8f46-940d-4527-98a7-27894a23974f",
   "metadata": {},
   "source": [
    "## Revoking permissions\n",
    "\n",
    "We can revoke specific permssions via a PATCH request\n",
    "by specifying the user (or group) and one or more scopes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "13d54943-e95a-4222-a67d-8f3832b70e3c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [200]>"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "options = {\n",
    "    \"user\": \"shared-with\",\n",
    "    \"scopes\": ['shares!server=sharer/'],\n",
    "}\n",
    "\n",
    "session.patch(shares_url, data=json.dumps(options))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b2b2619-3bb4-41a7-852b-de019f49185e",
   "metadata": {},
   "source": [
    "If scopes are unspecified, all permissions are revoked:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "1d8997fa-7093-44dd-abdf-405fe8cc7fcd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [200]>"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "options = {\n",
    "    \"user\": \"shared-with\",\n",
    "}\n",
    "\n",
    "session.patch(shares_url, data=json.dumps(options))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb4e37a4-d206-420b-ad21-93a15c65bbd9",
   "metadata": {},
   "source": [
    "_All_ shared access can be revoked via a DELETE request to the shares URL:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "203ec184-df77-493b-bd8f-e6bb461abe7e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Response [204]>"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.delete(shares_url)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "129d98a3-df16-4607-8a9d-784843a7eeaf",
   "metadata": {},
   "source": [
    "and we can see that nobody has shared access anymore"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d40f97b8-171f-4958-9cbf-bcc08a5f0db7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'items': [],\n",
       " '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "session.get(shares_url).json()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
